This change follows I39cc2a735d9625c87bf4ede6f5fb0ec441d47dcc.
docs/extension.schema.v1.json
docs/extension.schema.v2.json
includes/registration/ExtensionProcessor.php
* The new extension attribute 'OOUIThemePaths' can be used to define
custom OOUI themes. See I9187a63e509b601b8558ea82850fa828e5c8cc0a
for an example usage.
includes/resourceloader/ResourceLoaderOOUIModule.php
* Add support for 'OOUIThemePaths'.
* Defining 'images' is now optional. I figure custom themes are
unlikely to have or need them.
* Use ResourceLoaderFilePath objects to allow skin-/extension-defined
OOUI module files to use skin/extension's base paths.
This was previously used to support $wgResourceModuleSkinStyles,
but only for 'skinStyles' - now ResourceLoaderFileModule needs
to also handle it for 'skinScripts', and ResourceLoaderImageModule
for 'images').
includes/resourceloader/ResourceLoaderFilePath.php
* Add getters for local/remote base paths, for when we need to
construct a new ResourceLoaderFilePath based on existing one.
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderImageModule.php
includes/resourceloader/ResourceLoaderOOUIImageModule.php
* Add or improve handling of ResourceLoaderFilePaths:
* Replace `(array)` casts with explicit array wrapping, to avoid
casting objects into associative arrays.
* Use getLocalPath() instead of string concatenation.
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderImageModuleTest.php
* Some basic checks for the above.
Bug: T100896
Change-Id: I74362f0fc215b26f1f104ce7bdbbac1e106736ad
"SkinOOUIThemes": {
"type": "object"
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
"type": "object",
"description": "Map of skin names to OOUI themes to use. Same format as ResourceLoaderOOUIModule::$builtinSkinThemeMap."
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
'ResourceFileModulePaths',
'ResourceModules',
'ResourceModuleSkinStyles',
+ 'OOUIThemePaths',
'QUnitTestModule',
'ExtensionMessagesFiles',
'MessagesDirs',
}
}
- foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles' ] as $setting ) {
+ foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
if ( isset( $info[$setting] ) ) {
foreach ( $info[$setting] as $name => $data ) {
if ( isset( $data['localBasePath'] ) ) {
if ( $defaultPaths ) {
$data += $defaultPaths;
}
- $this->globals["wg$setting"][$name] = $data;
+ if ( $setting === 'OOUIThemePaths' ) {
+ $this->attributes[$setting][$name] = $data;
+ } else {
+ $this->globals["wg$setting"][$name] = $data;
+ }
}
}
}
case 'debugScripts':
case 'styles':
case 'packageFiles':
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
case 'templates':
$hasTemplates = true;
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
// Collated lists of file paths
case 'languageScripts':
"'$key' given, string expected."
);
}
- $this->{$member}[$key] = (array)$value;
+ $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
}
break;
case 'deprecated':
// Ensure relevant template compiler module gets loaded
foreach ( $this->templates as $alias => $templatePath ) {
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$suffix = explode( '.', $alias );
$suffix = end( $suffix );
return $summary;
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getPath();
+ }
+
+ return $path;
+ }
+
/**
* @param string|ResourceLoaderFilePath $path
* @return string
foreach ( $this->templates as $alias => $templatePath ) {
// Alias is optional
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$localPath = $this->getLocalPath( $templatePath );
if ( file_exists( $localPath ) ) {
return "{$this->remoteBasePath}/{$this->path}";
}
+ /**
+ * @return string
+ */
+ public function getLocalBasePath() {
+ return $this->localBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRemoteBasePath() {
+ return $this->remoteBasePath;
+ }
+
/**
* @return string
*/
// Ensure that all files have common extension.
$extensions = [];
- $descriptor = (array)$this->descriptor;
+ $descriptor = is_array( $this->descriptor ) ? $this->descriptor : [ $this->descriptor ];
array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
- $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+ $extensions[] = pathinfo( $this->getLocalPath( $path ), PATHINFO_EXTENSION );
} );
$extensions = array_unique( $extensions );
if ( count( $extensions ) !== 1 ) {
*/
public function getPath( ResourceLoaderContext $context ) {
$desc = $this->descriptor;
- if ( is_string( $desc ) ) {
- return $this->basePath . '/' . $desc;
+ if ( !is_array( $desc ) ) {
+ return $this->getLocalPath( $desc );
}
if ( isset( $desc['lang'] ) ) {
$contextLang = $context->getLanguage();
if ( isset( $desc['lang'][$contextLang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$contextLang];
+ return $this->getLocalPath( $desc['lang'][$contextLang] );
}
$fallbacks = Language::getFallbacksFor( $contextLang, Language::STRICT_FALLBACKS );
foreach ( $fallbacks as $lang ) {
if ( isset( $desc['lang'][$lang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$lang];
+ return $this->getLocalPath( $desc['lang'][$lang] );
}
}
}
if ( isset( $desc[$context->getDirection()] ) ) {
- return $this->basePath . '/' . $desc[$context->getDirection()];
+ return $this->getLocalPath( $desc[$context->getDirection()] );
}
if ( isset( $desc['default'] ) ) {
- return $this->basePath . '/' . $desc['default'];
+ return $this->getLocalPath( $desc['default'] );
} else {
throw new MWException( 'No matching path found' );
}
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->basePath}/$path";
+ }
+
/**
* Get the extension of the image.
*
$this->definition = null;
if ( isset( $options['data'] ) ) {
- $dataPath = $this->localBasePath . '/' . $options['data'];
+ $dataPath = $this->getLocalPath( $options['data'] );
$data = json_decode( file_get_contents( $dataPath ), true );
$options = array_merge( $data, $options );
}
$this->images[$skin] = $this->images['default'] ?? [];
}
foreach ( $this->images[$skin] as $name => $options ) {
- $fileDescriptor = is_string( $options ) ? $options : $options['file'];
+ $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
$allowedVariants = array_merge(
( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
return array_map( [ __CLASS__, 'safeFileHash' ], $files );
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->localBasePath}/$path";
+ }
+
/**
* Extract a local base path from module definition information.
*
// Find the path to the JSON file which contains the actual image definitions for this theme
if ( $module ) {
$dataPath = $this->getThemeImagesPath( $theme, $module );
+ if ( !$dataPath ) {
+ return false;
+ }
} else {
// Backwards-compatibility for things that probably shouldn't have used this class...
$dataPath =
* @return array|false
*/
protected function readJSONFile( $dataPath ) {
- $localDataPath = $this->localBasePath . '/' . $dataPath;
+ $localDataPath = $this->getLocalPath( $dataPath );
if ( !file_exists( $localDataPath ) ) {
return false;
// Expand the paths to images (since they are relative to the JSON file that defines them, not
// our base directory)
$fixPath = function ( &$path ) use ( $dataPath ) {
- $path = dirname( $dataPath ) . '/' . $path;
+ if ( $dataPath instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ dirname( $dataPath->getPath() ) . '/' . $path,
+ $dataPath->getLocalBasePath(),
+ $dataPath->getRemoteBasePath()
+ );
+ } else {
+ $path = dirname( $dataPath ) . '/' . $path;
+ }
};
array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
if ( is_string( $value['file'] ) ) {
* Return a map of theme names to lists of paths from which a given theme should be loaded.
*
* Keys are theme names, values are associative arrays. Keys of the inner array are 'scripts',
- * 'styles', or 'images', and values are string paths.
+ * 'styles', or 'images', and values are paths. Paths may be strings or ResourceLoaderFilePaths.
*
* Additionally, the string '{module}' in paths represents the name of the module to load.
*
*/
protected static function getThemePaths() {
$themePaths = self::$builtinThemePaths;
+ $themePaths += ExtensionRegistry::getInstance()->getAttribute( 'OOUIThemePaths' );
+
+ list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths();
+
+ // Allow custom themes' paths to be relative to the skin/extension that defines them,
+ // like with ResourceModuleSkinStyles
+ foreach ( $themePaths as $theme => &$paths ) {
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $paths );
+ if ( $localBasePath !== $defaultLocalBasePath || $remoteBasePath !== $defaultRemoteBasePath ) {
+ foreach ( $paths as &$path ) {
+ $path = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+ }
+ }
+ }
+
return $themePaths;
}
/**
* Return a path to load given module of given theme from.
*
+ * The file at this path may not exist. This should be handled by the caller (throwing an error or
+ * falling back to default theme).
+ *
* @param string $theme OOUI theme name, for example 'WikimediaUI' or 'Apex'
* @param string $kind Kind of the module: 'scripts', 'styles', or 'images'
* @param string $module Module name, for valid values see $knownScriptsModules,
* $knownStylesModules, $knownImagesModules
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemePath( $theme, $kind, $module ) {
$paths = self::getThemePaths();
$path = $paths[$theme][$kind];
- $path = str_replace( '{module}', $module, $path );
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ str_replace( '{module}', $module, $path->getPath() ),
+ $path->getLocalBasePath(),
+ $path->getRemoteBasePath()
+ );
+ } else {
+ $path = str_replace( '{module}', $module, $path );
+ }
return $path;
}
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeScriptsPath( $theme, $module ) {
if ( !in_array( $module, self::$knownScriptsModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeStylesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownStylesModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeImagesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownImagesModules ) ) {
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>eye</title><path d="M10 7.5a2.5 2.5 0 1 0 2.5 2.5A2.5 2.5 0 0 0 10 7.5zm0 7a4.5 4.5 0 1 1 4.5-4.5 4.5 4.5 0 0 1-4.5 4.5zM10 3C3 3 0 10 0 10s3 7 10 7 10-7 10-7-3-7-10-7z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M17 6L3 1v18h2v-6.87L17 6z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M3 6l14-5v18h-2v-6.87L3 6z"/></svg>
\ No newline at end of file
--- /dev/null
+mw.test();
--- /dev/null
+body {
+ color: red;
+}
--- /dev/null
+body {
+ color: black;
+}
--- /dev/null
+<div></div>
);
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ),
+ 'skinStyles' => [
+ 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
+ ],
+ 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
+ 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
+ ] );
+ $expectedModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'styles' => 'style.css',
+ 'skinStyles' => [
+ 'vector' => 'skinStyle.css',
+ ],
+ 'scripts' => 'script.js',
+ 'templates' => 'template.html',
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
public static function providerGetTemplates() {
$modules = self::getModules();
];
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => new ResourceLoaderFilePath( 'eye.svg', $filePath, 'rlfilepath' ),
+ 'flag' => [
+ 'file' => [
+ 'ltr' => new ResourceLoaderFilePath( 'flag-ltr.svg', $filePath, 'rlfilepath' ),
+ 'rtl' => new ResourceLoaderFilePath( 'flag-rtl.svg', $filePath, 'rlfilepath' ),
+ ],
+ ],
+ ],
+ ] );
+ $expectedModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => 'eye.svg',
+ 'flag' => [
+ 'file' => [
+ 'ltr' => 'flag-ltr.svg',
+ 'rtl' => 'flag-rtl.svg',
+ ],
+ ],
+ ],
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
/**
* @dataProvider providerGetModules
* @covers ResourceLoaderImageModule::getStyles